﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.XLANGs.Mozart;
using Microsoft.XLANGs.RuntimeTypes;
using ESB.BAM.BLogic;
using Microsoft.XLANGs.BaseTypes;
using System.IO;
using Microsoft.BizTalk.Streaming;
using Microsoft.Practices.ESB.Exception.Management;
using System.Reflection;
using System.Xml;
using System.Xml.XPath;
using Microsoft.BizTalk.Bam.EventObservation;

namespace ESB.BAM.Interceptors
{
    /// <summary>
    /// This class overrides the OOTB NativeInterceptor class to add the handing of the orchestration events and how they are linked to the custom tracking.
    /// </summary>
    [Serializable, CLSCompliant(false)]
    public class ESBOrchestrationInterceptor : NativeInterceptor, ITrackInterceptor
    {
        #region Fields
        public TrackInstanceState _State = null;
        #endregion

        #region Construction
        /// <summary>
        /// Initializes a new instance of the <see cref="ESBOrchestrationInterceptor"/> class.
        /// </summary>
        public ESBOrchestrationInterceptor()
            : base()
        {
        }
        #endregion

        #region ITrackInterceptor members

        #region Properties

        #endregion

        #region Methods

        /// <summary>
        /// Called when [exception].
        /// </summary>
        /// <param name="e">The e.</param>
        /// <param name="scopes">The scopes.</param>
        /// <param name="trackData">The track data.</param>
        public new void OnException(Exception e, string[] scopes, System.IO.Stream trackData)
        {
            base.OnException(e, scopes, trackData);
        }

        /// <summary>
        /// Called when [message construction].
        /// </summary>
        /// <param name="msg">The MSG.</param>
        public new void OnMessageConstruction(Microsoft.XLANGs.BaseTypes.XLANGMessage msg)
        {
            base.OnMessageConstruction(msg);
        }

        /// <summary>
        /// Called when [service event].
        /// </summary>
        /// <param name="data">The data.</param>
        /// <param name="trackData">The track data.</param>
        public new void OnServiceEvent(EventData data, System.IO.Stream trackData)
        {
            base.OnServiceEvent(data, trackData);
        }

        /// <summary>
        /// Called when [service level exception].
        /// </summary>
        /// <param name="e">The e.</param>
        /// <param name="scopes">The scopes.</param>
        /// <param name="trackData">The track data.</param>
        /// <param name="isThrownFromDefaultHandler">if set to <c>true</c> [is thrown from default handler].</param>
        public new void OnServiceLevelException(Exception e, string[] scopes, System.IO.Stream trackData, bool isThrownFromDefaultHandler)
        {
            base.OnServiceLevelException(e, scopes, trackData, isThrownFromDefaultHandler);
        }

        /// <summary>
        /// Tracks the event handler.
        /// This is the core function used to track all orchestration based events.
        /// </summary>
        /// <param name="location">The location.</param>
        /// <param name="data">The data.</param>
        /// <param name="trackData">The track data.</param>
        public new void TrackEventHandler(Location location, EventData data, System.IO.Stream trackData)
        {
            base.TrackEventHandler(location, data, trackData);
            HandleTrackEvent(location, data, DateTime.UtcNow);
        }

        #endregion

        #endregion

        #region Methods

        /// <summary>
        /// Handles the track event.
        /// This is the main method used to handle the mapping between the orchestration events and the tracking solution.
        /// it is called before the shape is executed and after it is executed
        /// it also handles the initialization of the activity if not already initialized using the mapping with action initialize for the first executed shape with a valid mapping
        /// </summary>
        /// <param name="location">The location.</param>
        /// <param name="data">The data.</param>
        /// <param name="TimeStamp">The time stamp.</param>
        private void HandleTrackEvent(Location location, EventData data, DateTime TimeStamp)
        {
            var callToken = Microsoft.BizTalk.CAT.BestPractices.Framework.Instrumentation.TraceManager.TrackingComponent.TraceIn(location);
            try
            {
                if (location != null && location.ShapeID != Guid.Empty.ToString() && Microsoft.XLANGs.Core.Context.RootService != null)
                {
                    bool bIsEmptyIf = false;
                    string strOrchestrationName = Microsoft.XLANGs.Core.Context.RootService.Name;
                    string ShapeName = Microsoft.XLANGs.Core.Context.RootService.FriendlyNameFromShapeId(location.ShapeID);
                    if (_State != null && _State._bIsInitialized)
                    {
                        // We have already an initialized state (meaning we have a valid activity Id to link to)
                        if (this.IsContextEnd(data.Op) && (IsMsgConstruction(data.Op) || IsMsgReceive(data.Op) || IsMsgSend(data.Op)))
                        {
                            // Check if we have requirment to track data from messages (or even entire message bodies) at this shape
                            // Check only if we are executing a message construction, receive, or send shapes
                            // This does not need to be linked to a specific business milestone as it is added as reference data (Message bodies only)
                            EsbTrackingOrchestrationDataStep[] lOrchData = EsbTrackingOrchestrationDataStep.GetOrchestrationDataList(strOrchestrationName, ShapeName);
                            if (lOrchData != null && lOrchData.Length > 0 && data.Messages.Count > 0)
                            {
                                foreach (EsbTrackingOrchestrationDataStep dProfile in lOrchData)
                                {
                                    if (string.IsNullOrEmpty(dProfile.DataExtractionXPath)) continue;
                                    // Search all messages at the current shape
                                    // Only the last one will be tracked if we have mutiple matches (for business extracted data only)
                                    for (int i = 0; i < data.Messages.Count; i++)
                                    {
                                        XLANGMessage message = data.Messages[i] as XLANGMessage;
                                        if (message == null) continue;
                                        Stream msgStream = GetMessageStream(message);
                                        if (msgStream == null) continue;
                                        if (dProfile.DataExtractionXPath.ToUpper() == "BODY")
                                        {
                                            // Track entire message body
                                            string strMsgBody = ExtractMessageBody(msgStream);
                                            if (!string.IsNullOrEmpty(strMsgBody))
                                            {
                                                string DataName = dProfile.DataName;
                                                if (i > 0) DataName += (i.ToString());
                                                ESB.BAM.Wrappers.BAMAPIWrapper.UpdateActivityReference(null, dProfile.ActivityName, _State._ActivityId, DataName, strMsgBody);
                                            }
                                        }
                                        else
                                        {
                                            // Track specific business data extracted from the message using XPath
                                            string DataValue = ExtractDataValueXPath(msgStream, dProfile.DataExtractionXPath);
                                            if (!string.IsNullOrEmpty(DataValue))
                                            {
                                                ESB.BAM.Wrappers.BAMAPIWrapper.UpdateActivityData(null, dProfile.ActivityName, _State._ActivityId, dProfile.DataName, DataValue);
                                            }
                                        }
                                        msgStream.Seek(0, SeekOrigin.Begin);
                                    }
                                }
                            }
                        }
                    }
                    // Check if we are within a tracked if condition and have a send message shape within the branch or not
                    if (_State != null && _State._bIsInitialized && !string.IsNullOrEmpty(_State._EndIfEmptyIfShapeId) && this.IsContextEnd(data.Op) && IsMsgSend(data.Op))
                    {
                        _State._bIsHasSentMsg = true;
                    }
                    EsbTrackingProfileStep ProfileStep = EsbTrackingProfileStep.GetProfileStep(strOrchestrationName, ShapeName);
                    if (ProfileStep != null)
                    {
                        if (this.IsContextEnd(data.Op) && (IsMsgConstruction(data.Op) || IsMsgSend(data.Op) || IsMsgReceive(data.Op)))
                        {
                            // Check if we have an InitId action mapping at the current shape and if this shape is a message contrsuction shape or not
                            // This will extract the activity Id from the message and store it in the state and mark the state as initialized
                            if (ProfileStep.Action == ItineraryServiceActionEnum.InitId && !string.IsNullOrEmpty(ProfileStep.MsgIdXPath))
                            {
                                _State = new TrackInstanceState();
                                for (int i = 0; i < data.Messages.Count; i++)
                                {
                                    XLANGMessage message = data.Messages[i] as XLANGMessage;
                                    if (message == null) continue;
                                    Stream msgStream = GetMessageStream(message);
                                    if (msgStream == null) continue;
                                    string id = ExtractDataValueXPath(msgStream, ProfileStep.MsgIdXPath);
                                    if (!string.IsNullOrEmpty(id))
                                    {
                                        _State._ActivityId = id;
                                        _State._bIsInitialized = true;
                                        // Get the first found Id that is not empty
                                        break;
                                    }
                                }
                            }
                        }
                        if (_State != null && _State._bIsInitialized)
                        {
                            // This is an already initialized state
                            if (ProfileStep.Action == ItineraryServiceActionEnum.EndIfEmpty && this.IsIf(data.Op))
                            {
                                // Do we need to end if empty at this if shape
                                // This is used to end the activity if we do not invoke the finalize itinerary by sending a message to the ESB on-ramp
                                if (this.IsContextBegin(data.Op) && string.IsNullOrEmpty(_State._EndIfEmptyIfShapeId))
                                {
                                    // If at the start of the shape then add it to the state to track if sends any messages within its branch or not
                                    _State._EndIfEmptyIfShapeId = location.ShapeID;
                                    _State._bIsHasSentMsg = false;
                                }
                                if (this.IsContextEnd(data.Op) && _State._EndIfEmptyIfShapeId == location.ShapeID)
                                {
                                    // This is at the end of the if shape
                                    if (_State._bIsHasSentMsg)
                                    {
                                        // It did not send any messages to it is considered as empty
                                        bIsEmptyIf = false;
                                    }
                                    else
                                    {
                                        //  It has already sent a message so it is not an empty if branch
                                        bIsEmptyIf = true;
                                    }
                                    // Reset the search state
                                    _State._EndIfEmptyIfShapeId = "";
                                    _State._bIsHasSentMsg = false;
                                }
                            }
                            if (this.IsContextEnd(data.Op))
                            {
                                // Only handles at the end of shapes
                                switch (ProfileStep.Action)
                                {
                                    case ItineraryServiceActionEnum.Start:
                                        throw new ApplicationException("Start activity is not supported from within an orchestration.");
                                    case ItineraryServiceActionEnum.ChangeId:
                                        throw new ApplicationException("Change ID is not supported from within an orchestration.");
                                    case ItineraryServiceActionEnum.InitId:
                                    case ItineraryServiceActionEnum.Update:
                                        ESB.BAM.Wrappers.BAMAPIWrapper.UpdateActivity(null, ProfileStep.ActivityName, _State._ActivityId, ProfileStep.MilestoneName, TimeStamp);
                                        break;
                                    case ItineraryServiceActionEnum.End:
                                        ESB.BAM.Wrappers.BAMAPIWrapper.UpdateActivity(null, ProfileStep.ActivityName, _State._ActivityId, ProfileStep.MilestoneName, TimeStamp);
                                        ESB.BAM.Wrappers.BAMAPIWrapper.EndActivity(null, ProfileStep.ActivityName, _State._ActivityId);
                                        _State._ActivityId = "";
                                        _State._bIsInitialized = false;
                                        _State = null;
                                        break;
                                    case ItineraryServiceActionEnum.EndIfEmpty:
                                        if (bIsEmptyIf)
                                        {
                                            ESB.BAM.Wrappers.BAMAPIWrapper.UpdateActivity(null, ProfileStep.ActivityName, _State._ActivityId, ProfileStep.MilestoneName, TimeStamp);
                                            ESB.BAM.Wrappers.BAMAPIWrapper.EndActivity(null, ProfileStep.ActivityName, _State._ActivityId);
                                            _State._ActivityId = "";
                                            _State._bIsInitialized = false;
                                            _State = null;
                                        }
                                        break;
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Microsoft.BizTalk.CAT.BestPractices.Framework.Instrumentation.TraceManager.TrackingComponent.TraceError(ex, true, callToken);
                EventLogger.LogMessage(EventFormatter.FormatEvent(MethodBase.GetCurrentMethod(), ex), System.Diagnostics.EventLogEntryType.Error, 0x17cb);
            }
            Microsoft.BizTalk.CAT.BestPractices.Framework.Instrumentation.TraceManager.TrackingComponent.TraceOut(callToken);
        }

        /// <summary>
        /// Extracts the message body.
        /// </summary>
        /// <param name="MsgStream">The MSG stream.</param>
        /// <returns></returns>
        private string ExtractMessageBody(Stream MsgStream)
        {
            XmlReaderSettings settings = new XmlReaderSettings()
            {
                ConformanceLevel = ConformanceLevel.Document,
                IgnoreWhitespace = true,
                ValidationType = ValidationType.None,
                IgnoreProcessingInstructions = true,
                IgnoreComments = true,
                CloseInput = false
            };
            MsgStream.Seek(0, SeekOrigin.Begin);
            XmlReader reader = XmlReader.Create(MsgStream, settings);
            string strMsgBody = null;
            if (reader.Read())
            {
                strMsgBody = reader.ReadOuterXml();
            }
            MsgStream.Seek(0, SeekOrigin.Begin);
            return strMsgBody;
        }

        /// <summary>
        /// Extracts the data value using Xpath.
        /// </summary>
        /// <param name="MsgStream">The MSG stream.</param>
        /// <param name="MsgXPath">The MSG Xpath.</param>
        /// <returns></returns>
        private string ExtractDataValueXPath(Stream MsgStream, string MsgXPath)
        {
            XmlReaderSettings settings = new XmlReaderSettings()
            {
                ConformanceLevel = ConformanceLevel.Document,
                IgnoreWhitespace = true,
                ValidationType = ValidationType.None,
                IgnoreProcessingInstructions = true,
                IgnoreComments = true,
                CloseInput = false
            };
            MsgStream.Seek(0, SeekOrigin.Begin);
            XmlReader reader = XmlReader.Create(MsgStream, settings);
            string strValue = null;
            if (!string.IsNullOrEmpty(MsgXPath))
            {
                if (reader.Read())
                {
                    XPathDocument xPathDoc = new XPathDocument(reader);
                    XPathNavigator xNavigator = xPathDoc.CreateNavigator();
                    XPathNodeIterator xNodes = xNavigator.Select(MsgXPath);
                    if (xNodes.Count != 0 && xNodes.MoveNext())
                    {
                        strValue = xNodes.Current.Value;
                    }
                }
            }
            MsgStream.Seek(0, SeekOrigin.Begin);
            return strValue;
        }

        /// <summary>
        /// Gets the message stream.
        /// </summary>
        /// <param name="msg">The MSG.</param>
        /// <returns></returns>
        private Stream GetMessageStream(Microsoft.XLANGs.BaseTypes.XLANGMessage msg)
        {
            if (msg.Count <= 0) throw new ArgumentException("XLANGMessage does not have a body part");
            Stream stream = msg[0].RetrieveAs(typeof(Stream)) as Stream;

            if (!stream.CanSeek)
            {
                ReadOnlySeekableStream readStream = new ReadOnlySeekableStream(stream);

                stream = readStream;
            }
            return stream;
        }

        /// <summary>
        /// Determines whether the specified op is code.
        /// </summary>
        /// <param name="op">The op.</param>
        /// <returns>
        ///   <c>true</c> if the specified op is code; otherwise, <c>false</c>.
        /// </returns>
        private bool IsCode(Operation op)
        {
            return ((op & Microsoft.XLANGs.RuntimeTypes.Operation.Expression) > 0);
        }

        /// <summary>
        /// Determines whether the specified op is if.
        /// </summary>
        /// <param name="op">The op.</param>
        /// <returns>
        ///   <c>true</c> if the specified op is if; otherwise, <c>false</c>.
        /// </returns>
        private bool IsIf(Operation op)
        {
            return ((op & Microsoft.XLANGs.RuntimeTypes.Operation.If) > 0);
        }

        /// <summary>
        /// Determines whether [is context begin] [the specified op].
        /// </summary>
        /// <param name="op">The op.</param>
        /// <returns>
        ///   <c>true</c> if [is context begin] [the specified op]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsContextBegin(Microsoft.XLANGs.RuntimeTypes.Operation op)
        {
            return ((op & Microsoft.XLANGs.RuntimeTypes.Operation.Start) > 0);
        }

        /// <summary>
        /// Determines whether [is context end] [the specified op].
        /// </summary>
        /// <param name="op">The op.</param>
        /// <returns>
        ///   <c>true</c> if [is context end] [the specified op]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsContextEnd(Microsoft.XLANGs.RuntimeTypes.Operation op)
        {
            return ((op & Microsoft.XLANGs.RuntimeTypes.Operation.End) > 0);
        }

        /// <summary>
        /// Determines whether [is message received] [the specified op].
        /// </summary>
        /// <param name="op">The op.</param>
        /// <returns>
        ///   <c>true</c> if [is message received] [the specified op]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsMessageReceived(Microsoft.XLANGs.RuntimeTypes.Operation op)
        {
            return (op == (Microsoft.XLANGs.RuntimeTypes.Operation.Receive | Microsoft.XLANGs.RuntimeTypes.Operation.End));
        }

        /// <summary>
        /// Determines whether [is message sent] [the specified op].
        /// </summary>
        /// <param name="op">The op.</param>
        /// <returns>
        ///   <c>true</c> if [is message sent] [the specified op]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsMessageSent(Microsoft.XLANGs.RuntimeTypes.Operation op)
        {
            return (op == (Microsoft.XLANGs.RuntimeTypes.Operation.Send | Microsoft.XLANGs.RuntimeTypes.Operation.End));
        }

        /// <summary>
        /// Determines whether [is while body] [the specified op].
        /// </summary>
        /// <param name="op">The op.</param>
        /// <returns>
        ///   <c>true</c> if [is while body] [the specified op]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsWhileBody(Microsoft.XLANGs.RuntimeTypes.Operation op)
        {
            if (op != (Microsoft.XLANGs.RuntimeTypes.Operation.WhileBody | Microsoft.XLANGs.RuntimeTypes.Operation.End))
            {
                return (op == (Microsoft.XLANGs.RuntimeTypes.Operation.WhileBody | Microsoft.XLANGs.RuntimeTypes.Operation.Start));
            }
            return true;
        }

        /// <summary>
        /// Determines whether [is MSG construction] [the specified op].
        /// </summary>
        /// <param name="op">The op.</param>
        /// <returns>
        ///   <c>true</c> if [is MSG construction] [the specified op]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsMsgConstruction(Microsoft.XLANGs.RuntimeTypes.Operation op)
        {
            return ((op & Microsoft.XLANGs.RuntimeTypes.Operation.Construct) > 0);
        }

        /// <summary>
        /// Determines whether [is MSG send] [the specified op].
        /// </summary>
        /// <param name="op">The op.</param>
        /// <returns>
        ///   <c>true</c> if [is MSG send] [the specified op]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsMsgSend(Operation op)
        {
            return ((op & Microsoft.XLANGs.RuntimeTypes.Operation.Send) > 0);
        }

        /// <summary>
        /// Determines whether [is MSG receive] [the specified op].
        /// </summary>
        /// <param name="op">The op.</param>
        /// <returns>
        ///   <c>true</c> if [is MSG receive] [the specified op]; otherwise, <c>false</c>.
        /// </returns>
        private bool IsMsgReceive(Operation op)
        {
            return ((op & Microsoft.XLANGs.RuntimeTypes.Operation.Receive) > 0);
        }

        #endregion
    }
}
